// Copyright 2022 Jeroen van Nugteren

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// include header
#include "dfpolygon.hh"

// common headers
#include "rat/common/extra.hh"

// code specific to Rat
namespace rat{namespace dm{

	DFPolygon::DFPolygon(){
		set_name("Polygon");
	}

	// constructor
	DFPolygon::DFPolygon(const arma::Mat<fltp> &v) : DFPolygon(){
		// set to self
		set_coords(v);
	}

	// factory
	ShDFPolygonPr DFPolygon::create(){
		return std::make_shared<DFPolygon>();
	}

	// factory
	ShDFPolygonPr DFPolygon::create(const arma::Mat<fltp> &v){
		return std::make_shared<DFPolygon>(v);
	}

	// setters
	void DFPolygon::set_scale_factor(const fltp scale_factor){
		scale_factor_ = scale_factor;
	}

	void DFPolygon::set_coords(const arma::Mat<fltp> &v){
		v_ = v;
	}

	// getters
	fltp DFPolygon::get_scale_factor()const{
		return scale_factor_;
	}

	const arma::Mat<fltp>& DFPolygon::get_coords()const{
		return v_;
	}

	// perimeter function
	ShPerimeterPr DFPolygon::create_perimeter(const fltp delem) const{
		// create scaled points
		const arma::Mat<fltp> P = scale_factor_*v_;

		// allocate storage for coordinates
		arma::field<arma::Mat<fltp> > Rn_fld(1,P.n_rows);

		// walk over points
		for(arma::uword i=0;i<P.n_rows;i++){
			// get loop around indices
			const arma::uword idx1 = i;
			const arma::uword idx2 = (i+1)%P.n_rows;

			// calculate distance between points
			const fltp point_separation = std::hypot(P(idx2,0) - P(idx1,0), P(idx2,1) - P(idx1,1));
			const arma::uword nelem = arma::uword(std::ceil(point_separation/delem));

			// create x and y using linear spacing
			const arma::Row<fltp> x = arma::linspace<arma::Row<fltp> >(P(idx1,0), P(idx2,0), nelem+1);
			const arma::Row<fltp> y = arma::linspace<arma::Row<fltp> >(P(idx1,1), P(idx2,1), nelem+1);

			// set to matrix
			Rn_fld(i) = arma::join_vert(x.cols(0,nelem-1),y.cols(0,nelem-1));
		}

		// combine
		const arma::Mat<fltp> Rn = cmn::Extra::field2mat(Rn_fld);

		// create element matrix
		const arma::Mat<arma::uword> n=arma::join_vert(
			arma::regspace<arma::Row<arma::uword> >(0,Rn.n_cols-1),
			arma::shift(arma::regspace<arma::Row<arma::uword> >(0,Rn.n_cols-1),-1,1));

		// create perimeter and return it
		return Perimeter::create(Rn,n);
	}


	// get bounding box
	arma::Mat<fltp>::fixed<2,2> DFPolygon::get_bounding() const{
		// allocate
		arma::Mat<fltp>::fixed<2,2> Mb;
		
		// bounding in x
		Mb.col(0) = arma::Col<fltp>::fixed<2>{arma::min(v_.col(0)),arma::max(v_.col(0))};
		
		// bounding in y
		Mb.col(1) = arma::Col<fltp>::fixed<2>{arma::min(v_.col(1)),arma::max(v_.col(1))};

		// return box
		return scale_factor_*Mb;
	}


	// get fixed points (all nodes are fixed)
	arma::Mat<fltp> DFPolygon::get_fixed(const fltp /*abstol*/) const{
		return scale_factor_*v_;
	}

	// point in polygon check
	arma::Col<arma::uword> DFPolygon::inpolygon(
		const arma::Mat<fltp> &v, const arma::Mat<fltp> &p){
		// get counters
		const arma::uword nv = v.n_rows;
		const arma::uword np = p.n_rows;

		// allocate checklist
		arma::Col<arma::uword> chk(np,arma::fill::zeros);

		// walk over points
		for(arma::uword k=0;k<np;k++){
			// walk over edges
			for(arma::uword i=0,j=nv-1;i<nv;j=i++){
				if(((v(i,1)>p(k,1))!=(v(j,1)>p(k,1))) && (p(k,0)<(v(j,0)-v(i,0))*(p(k,1)-v(i,1))/(v(j,1)-v(i,1))+v(i,0)))
					chk(k) = !chk(k);
			}
		}

		// return list
		return chk;
	}


	// distance function
	arma::Col<fltp> DFPolygon::calc_distance(const arma::Mat<fltp> &p) const{
		// check self
		is_valid(true);

		// number of points and number of sections
		arma::uword np = p.n_rows;
		arma::uword nv = v_.n_rows;

		// distance matrix
		arma::Mat<fltp> ds(np,nv);

		// walk over sections
		for(arma::uword i=0,j=nv-1;i<nv;j=i++){
			// get vertices
			const arma::Row<fltp>::fixed<2> v1 = scale_factor_*v_.row(i);
			const arma::Row<fltp>::fixed<2> v2 = scale_factor_*v_.row(j);
				
			// calculate edge vector
			const arma::Row<fltp>::fixed<2> dv = v2-v1;

			// walk over points
			for(arma::uword ip=0;ip<np;ip++){
				// get point
				const arma::Row<fltp>::fixed<2> myp = p.row(ip);

				// calculate vector from first vertex to point
				const arma::Row<fltp>::fixed<2> dp = myp - v1;

				// calculate dot products
				const fltp c1 = arma::accu(dv%dp);
				const fltp c2 = arma::accu(dv%dv);
				
				// distance to projected point on line
				ds(ip,i) = std::sqrt(arma::accu(arma::square(
					myp-(v1+std::min(RAT_CONST(1.0),std::max(RAT_CONST(0.0),c1/c2))*dv))));
			}
		}

		// check which points are inside the polygon
		const arma::Col<fltp> sign = 2*arma::conv_to<arma::Col<fltp> >::from(inpolygon(scale_factor_*v_,p)) - 1;

		// check output
		assert(sign.is_finite());

		// signed distance
		arma::Col<fltp> sds = -arma::min(ds,1)%sign;

		// negative distance inside
		return sds;
	}

	// calculate area of the polygon
	fltp DFPolygon::calculate_area()const{
		// get number of points
		const arma::uword num_points = v_.n_rows;
	
		// check number of points
		if(num_points<=2)return RAT_CONST(0.0);

		// allocate
		fltp area = RAT_CONST(0.0);

		// walk over edges
		for(arma::uword i=0;i<num_points;i++){
			// get coordinates
			const fltp x1 = v_(i,0), x2 = v_((i+1)%num_points,0);
			const fltp y1 = v_(i,1), y2 = v_((i+1)%num_points,1);

			// add area using cross product
			area += (x1*y2) - (x2*y1);
		}

		// return area divided by 2 because it is triangles
		return std::abs(area/2);
	}


	// validity check
	bool DFPolygon::is_valid(const bool enable_throws)const{
		// check user input
		if(v_.is_empty()){if(enable_throws){rat_throw_line("coordinates are not set");} return false;};
		if(!v_.is_finite()){if(enable_throws){rat_throw_line("coordinates are not finite");} return false;};
		if(v_.n_cols!=2){if(enable_throws){rat_throw_line("wrong number of columns");} return false;};
		if(v_.n_rows<=2){if(enable_throws){rat_throw_line("need at least 3 points to make a polygon");} return false;};
		if(scale_factor_==0){if(enable_throws){rat_throw_line("scale factor can not be zero");} return false;};
		if(arma::any(arma::all(arma::diff(v_,1,0)==RAT_CONST(0.0),1))){if(enable_throws){rat_throw_line("consecutive points must have non-zero spacing");} return false;};
		if(calculate_area()<RAT_CONST(1e-14)){if(enable_throws){rat_throw_line("polygon has no area");} return false;};
		return true;
	}

	// type string for serialization
	std::string DFPolygon::get_type(){
		return "rat::dm::dfpolygon";
	}

	// method for serialization into json
	void DFPolygon::serialize(
		Json::Value &js, cmn::SList &list) const{
		
		// parent
		DistFun::serialize(js,list);

		// store type ID
		js["type"] = get_type();
		js["scale_factor"] = scale_factor_;
		for(arma::uword i=0;i<v_.n_rows;i++){
			js["x"].append(v_(i,0));
			js["y"].append(v_(i,1));
		}
	}

	// method for deserialisation from json
	void DFPolygon::deserialize(
		const Json::Value &js, cmn::DSList &list, 
		const cmn::NodeFactoryMap &factory_list, 
		const boost::filesystem::path &pth){

		// parent
		DistFun::deserialize(js,list,factory_list,pth);

		// get values
		if(js.isMember("scale_factor"))scale_factor_ = js["scale_factor"].ASFLTP();
		v_.set_size(js["x"].size(),2);
		arma::uword idx = 0; auto it1 = js["x"].begin(); auto it2 = js["y"].begin();
		for(;it1!=js["x"].end() && it2!=js["y"].end();it1++,it2++,idx++){
			v_(idx,0) = (*it1).ASFLTP(); 
			v_(idx,1) = (*it2).ASFLTP();
		}
	}

}}